# -*- coding: utf-8 -*-

# @File   : common
# @Author : Harold
# @Date   : 2018/2/26

from django.utils.translation import ugettext, ugettext_noop as _
from enum import Enum, IntEnum
from apps.storage.models.template import TemplateFieldEnum
from apps.storage.models.file import ObjectFile, ObjectFileType, ObjectContentType
import os


class DataMode(IntEnum):
    """
    Excel操作模式
    """
    READ = 1  # 导入数据
    WRITE = 2  # 导出数据
    TEMPLATE = 3  # 生成导入模板

    @property
    def description(self):
        if self == 1:
            return "read"
        elif self == 2:
            return "write"
        elif self == 3:
            return "template"


class DataMetaFieldEnum(Enum):
    """
    DataMeta表的字段
    """
    data_id = _('Data ID')
    title = _("Title")
    doi = _("DOI")
    abstract = _("Abstract")
    purpose = _("Purpose")
    material_class = _("Material Class")
    template_name = _("Template Name")
    keywords = _("Keywords")
    author = _("Author")
    created_at = _("Created at")
    source = _("Source")
    reference = _("Reference")
    project = _("Project")
    others = _("Other Info")
    template_id = _("Template ID")

    @property
    def description(self):
        return ugettext(self.value)

    def get_value(self, data_meta):
        """
        从DataMeta对象中获取字段的值
        :param data_meta:
        :return:
        """
        if self == self.__class__.data_id:
            return data_meta.id
        elif self == self.__class__.title:
            return data_meta.title
        elif self == self.__class__.material_class:
            return data_meta.category.name
        elif self == self.__class__.template_id:
            return str(data_meta.tid)
        elif self == self.__class__.keywords:
            return ', '.join(data_meta.keywords)
        elif self == self.__class__.author:
            return data_meta.author.username
        elif self == self.__class__.created_at:
            return data_meta.add_time.strftime('%Y-%m-%d %H:%M:%S')
        elif self == self.__class__.source:
            return data_meta.source.get('source')
        elif self == self.__class__.reference:
            return data_meta.source.get('reference')
        elif self == self.__class__.others:
            return data_meta.source.get('others')
        elif self == self.__class__.project:
            return data_meta.source.get('project')
        elif self == self.__class__.doi:
            return data_meta.doi
        elif self == self.__class__.abstract:
            return data_meta.abstract
        elif self == self.__class__.purpose:
            return data_meta.purpose
        # elif self == self.__class__.template_name:
        #     template = Template.objects.get(id=data_meta.tid)
        #     return template.title

    @property
    def field_raw_name(self):
        """
        获取DataMeta JSON中的字段名
        :return:
        """
        return self.name

    @staticmethod
    def writer_iterator(with_id=False):
        """
        导出Excel时DataMeta页填写的字段
        :return: 迭代器
        """
        _meta_fields = DataMetaFieldEnum.writer_meta_fields()
        if with_id:
            _meta_fields = ["Data ID"] + _meta_fields
        index = 0
        while index < len(_meta_fields):
            yield DataMetaFieldEnum(_meta_fields[index])
            index += 1

    @staticmethod
    def reader_iterator(with_id=False):
        """
        生成导入模板时DataMeta页填写的字段
        :return: 迭代器
        """
        _meta_fields = DataMetaFieldEnum.reader_meta_fields()
        if with_id:
            _meta_fields = ["Data ID"] + _meta_fields
        index = 0
        while index < len(_meta_fields):
            yield DataMetaFieldEnum(_meta_fields[index])
            index += 1

    @staticmethod
    def reader_meta_fields():
        return ["Title", "DOI", "Abstract", "Purpose", "Keywords", "Source", "Reference", "Project",
                "Other Info"]

    @staticmethod
    def writer_meta_fields():
        return ["Title", "Material Class", "DOI", "Abstract", "Purpose", "Keywords", "Author", "Created at",
                "Source", "Reference", "Project", "Other Info"]


class ParsingErrorEnum(Enum):
    FIELD_MISSING = _("Field %(o1)s is missing. ")
    UNKNOWN_FIELD = _("Unknown field \"%(o1)s\".")
    VALUE_MISSING = _('Value for field "%(o1)s" is required but is missing.')
    INVALID_VALUE = _('Invalid value "%(o1)s" for type "%(o2)s".')
    NO_ELEMENTS_IN_ARRAY = _('No elements found in array "%(o1)s".')
    NO_META = _('Data "%(o1)s" has no metadata.')
    NO_DATA = _('Data "%(o1)s" has metadata but no data content.')
    TEMPLATE_NOT_FOUND = _('Template "%(o1)s" not found.')
    FILE_NOT_FOUND = _('File "%(o1)s" does not exist the archive file.')
    BAD_TEMPLATE = _(
        "Template file is corrupted. Please re-generate a template. "
        # "(Perhaps you've modified some cells that you shouldn't have)"
    )
    INVALID_CHOICE_VALUE = _('Value "%(o1)s" is not a valid choice. Allowed choices are %(o2)s.')
    XMLSyntaxError = _("Bad XML Syntax: %(o1)s.")
    NODE_MISSING = _("Node \"%(o1)s\" is missing from node \"%(o2)s\".")
    XML_ATTRIBUTE_MISSING = _("Attribute \"%(o1)s\" is missing from node \"%(o2)s\".")
    TEMPLATE_DOES_NOT_EXIST = _("Template (id=%(o1)s) does not exist.")


class ParsingError(Exception):

    def __init__(self, error, **kwargs):
        self.message = error.value % kwargs
        self.error = error

    def __str__(self):
        return self.message


class Parser:
    def __init__(self, file_dir: str = None):
        self._file_dir = file_dir
        self._created_files = []  # 已写入数据库的文件ID列表，一旦解析时发生错误，删除所有文件
        self._file_path_id_map = {}  # 已写入数据库的文件路径，防止重复上传相同文件

    def _roll_back(self):
        ObjectFile.objects.filter(id__in=self._created_files).delete()

    def parse(self, path_str: str, field_meta: dict, value_string: str):
        """
        解析基本类型
        :param path_str: 字段path
        :param field_meta: 字段元信息
        :param value_string: 用户提供的值
        :return: 解析生成的值
        """
        try:
            required = field_meta.get('misc', {}).get('r')
            if value_string is None or value_string.strip() == "":
                if required:
                    raise ParsingError(ParsingErrorEnum.VALUE_MISSING, o1=path_str)
                else:
                    return None
            t = TemplateFieldEnum(field_meta['t'])
            if t.is_number:
                try:
                    return float(value_string)
                except ValueError:
                    raise ParsingError(ParsingErrorEnum.INVALID_VALUE, o1=value_string, o2=t.description)
            elif t.is_choice:
                choices = []
                choices.extend(field_meta['misc'].get('opt', []))
                for group in field_meta['misc'].get('grp', []):
                    choices.extend(group.get('items', []))
                if str(value_string) not in choices:
                    raise ParsingError(ParsingErrorEnum.INVALID_CHOICE_VALUE, o1=value_string,
                                       o2=', '.join(choices))
                return str(value_string)
            elif t.is_range:
                range_type = field_meta['misc']['type']
                if range_type == 1:
                    try:
                        err_data = value_string.split('±')
                        val = float(err_data[0])
                        err = float(err_data[1])
                        return {'val': val, 'err': err}
                    except (IndexError, AttributeError, TypeError, ValueError):
                        raise ParsingError(ParsingErrorEnum.INVALID_VALUE, o1=value_string,
                                           o2=ugettext("Error Type"))
                else:
                    try:
                        trimmed = value_string[1:-1]
                        bounds = trimmed.split(',')
                        return {'lb': float(bounds[0]), 'ub': float(bounds[1])}
                    except (IndexError, AttributeError, TypeError, ValueError):
                        raise ParsingError(ParsingErrorEnum.INVALID_VALUE, o1=value_string,
                                           o2=ugettext("Interval Type"))
            elif t.is_file or t.is_image:
                if self._file_dir is None:
                    return None
                value_list = []
                file_list = value_string.split(',')
                for filename in file_list:
                    try:
                        file_path = os.path.join(self._file_dir, filename)
                        with open(file_path, 'rb') as f:
                            if file_path in self._file_path_id_map:
                                value_list.append(self._file_path_id_map[file_path])
                                continue
                            if t.is_image:
                                file = ObjectFile.add(ObjectFileType.UPLOADED, ObjectContentType.DATA_IMAGE, f,
                                                      name=filename, path=file_path
                                                      )
                            else:
                                file = ObjectFile.add(ObjectFileType.UPLOADED, ObjectContentType.DATA_FILE, f,
                                                      name=filename, path=file_path)
                            self._created_files.append(file.id)
                            self._file_path_id_map[file_path] = str(file.id)
                            value_list.append(file.get_file_url())
                    except FileNotFoundError:
                        raise ParsingError(ParsingErrorEnum.FILE_NOT_FOUND, o1=filename)
                return value_list
            else:
                return str(value_string)
        except ParsingError:
            self._roll_back()
            raise

    def next_data(self):
        self._created_files.clear()
        self._file_path_id_map.clear()


def exclude_fields(template, exclude_list):
    """
    从模板中删除不需要导出的字段
    :param template: 模板
    :param exclude_list: 不导出的字段列表
    :return:
    """

    def _should_exclude(t, misc, path_list):
        path_str = '.'.join(path_list)
        if path_str in exclude_list:
            return True
        t = TemplateFieldEnum(t)
        if t.is_table:
            temp_headers = misc["_head"][:]
            for header in temp_headers:
                if _should_exclude(t=misc[header]['t'], misc=misc[header].get('misc', {}),
                                   path_list=path_list + [header]):
                    misc["_head"].remove(header)
                    misc.pop(header)
        elif t.is_container:
            temp_content = misc
            temp_ord = temp_content['_ord'][:]
            for temp_field in temp_ord:
                if _should_exclude(t=temp_content[temp_field]['t'], misc=temp_content[temp_field].get('misc', {}),
                                   path_list=[temp_field]):
                    temp_content['_ord'].remove(temp_field)
                    temp_content.pop(temp_field)
        elif t.is_array:
            element_path_list = path_list + [TemplateFieldEnum(misc['t']).description]
            _should_exclude(t=misc['t'], misc=misc.get('misc', {}), path_list=element_path_list)
        elif t.is_generator:
            temp_content = misc
            temp_ord = temp_content['_opt'][:]
            for temp_field in temp_ord:
                if _should_exclude(t=temp_content[temp_field]['t'], misc=temp_content[temp_field].get('misc', {}),
                                   path_list=[temp_field]):
                    temp_content['_opt'].remove(temp_field)
                    temp_content.pop(temp_field)
        return False

    content = template.content
    _ord = content['_ord'][:]
    for field in _ord:
        if _should_exclude(t=content[field]['t'], misc=content[field].get('misc', {}), path_list=[field]):
            content['_ord'].remove(field)
            content.pop(field)
    return template


def choices_to_list(choice_field_misc):
    choices = []
    choices.extend(choice_field_misc.get('opt', []))
    for group in choice_field_misc.get('grp', []):
        choices.extend(group.get('items', []))
    return choices


class Serialzer:
    pass


class TableFieldSerialer:
    pass


# lyp新加
from typing import Callable
import apps.storage.models.data as data_models

class FieldSerializer:
    """
    抽象基类
    """

    def serialize_field(self, mongo_value, field_meta):
        t = field_meta.field_type

        if t.is_string:
            return self.serialize_string(mongo_value, field_meta)
        elif t.is_number:
            return self.serialize_number(mongo_value, field_meta)
        elif t.is_range:
            return self.serialize_range(mongo_value, field_meta)
        elif t.is_image:
            return self.serialize_image(mongo_value, field_meta)
        elif t.is_file:
            return self.serialize_file(mongo_value, field_meta)
        elif t.is_choice:
            return self.serialize_choice(mongo_value, field_meta)

    def serialize_string(self, mongo_value, field_meta):
        return str(mongo_value)

    def serialize_number(self, mongo_value, field_meta):
        raise NotImplementedError

    def serialize_range(self, mongo_value, field_meta):
        raise NotImplementedError

    def serialize_image(self, mongo_value, field_meta):
        raise NotImplementedError

    def serialize_file(self, mongo_value, field_meta):
        raise NotImplementedError

    def serialize_choice(self, mongo_value, field_meta):
        return str(mongo_value)


class DataHandler:
    def __init__(self, mode: DataMode, template):
        self._mode = mode
        self._template = template
        self.meta_id_list = []

    def read_data(self, fp_or_path, uploaded_by, file_dir=None, progress_handler: Callable[[float], None] = None):
        raise NotImplementedError

    def write_data(self, data_meta):
        raise NotImplementedError

    def save(self, fp_or_path):
        raise NotImplementedError

    def generate_template(self, number_of_data=3, elements_per_array=1, rows_per_table=1):
        raise NotImplementedError

    def roll_back(self):
        data_models.DataMeta.importing_objects.filter(id__in=self.meta_id_list).delete()

    def filter_fields(self, excluded_fields=(), included_fields=()):
        if self._mode != DataMode.WRITE:
            raise ValueError("Cannot filter fields in write mode.")
        self._template.filter_fields(excluded_fields, included_fields)

    @property
    def template(self):
        return self._template
